const readFileAsArrayBuffer = async (file: File | Promise<ArrayBuffer>): Promise<ArrayBuffer> => {
  if (file instanceof Promise) {
    return file
  }
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result as ArrayBuffer);
    reader.onerror = () => reject(reader.error);
    reader.readAsArrayBuffer(file);
  });
}

export class FileHeaderAes {
  public async encrypt(file: File | Promise<ArrayBuffer>, password: string, progress?: (i: number) => void) {
    try {
      if (!password) {
        throw new Error('Please enter a password.')
      }
      const fileBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer;
      const salt = crypto.getRandomValues(new Uint8Array(16)); // Generate random salt
      const iv = crypto.getRandomValues(new Uint8Array(16)); // Generate random IV
      if (progress) {
        progress(20)
      }
      const cryptoKey = await this.deriveKey(password, salt);
      const encryptedContent = await crypto.subtle.encrypt(
          {
            name: "AES-CBC",
            iv: iv,
          },
          cryptoKey,
          fileBuffer
      );
      if (progress) {
        progress(70)
      }
      // Combine salt, IV, and encrypted content
      const encryptedFile = new Uint8Array(salt.byteLength + iv.byteLength + encryptedContent.byteLength);
      encryptedFile.set(salt, 0);
      encryptedFile.set(iv, salt.byteLength);
      encryptedFile.set(new Uint8Array(encryptedContent), salt.byteLength + iv.byteLength);
      return new Blob([encryptedFile], {type: "application/octet-stream"});
    } finally {
      if (progress) {
        progress(100)
      }
    }
  }

  private static readonly SALT_SIZE = 16;
  private static readonly IV_SIZE = 16;
  private static readonly BYTES_PREFIX = new TextEncoder().encode('4eb2b9a0234aa28c');

  // 从密码和盐生成密钥
  private async deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
    const encoder = new TextEncoder();
    const passwordKey = await crypto.subtle.importKey(
        'raw',
        encoder.encode(password),
        {name: 'PBKDF2'},
        false,
        ['deriveKey'],
    );

    return crypto.subtle.deriveKey(
        {
          name: 'PBKDF2',
          salt,
          iterations: 65536,
          hash: 'SHA-256',
        },
        passwordKey,
        {
          name: 'AES-CBC',
          length: 256,
        },
        false,
        ['encrypt', 'decrypt'],
    );
  }

  // 将字节数组转换为整数
  private static byteArrayToInt(byteArray: Uint8Array): number {
    return (byteArray[0] << 24) |
        (byteArray[1] << 16) |
        (byteArray[2] << 8) |
        byteArray[3];
  }

  private getSegmentEncryptData(fileBuffer: ArrayBuffer, progress?: (percent: number) => void): {
    encryptedContent: Uint8Array;
    iv: Uint8Array;
    salt: Uint8Array
  } {
    const byteLength = fileBuffer.byteLength;
    let offset = FileHeaderAes.BYTES_PREFIX.length;
    // 提取salt
    const salt = new Uint8Array(fileBuffer.slice(offset, offset + FileHeaderAes.SALT_SIZE));
    offset += FileHeaderAes.SALT_SIZE;
    progress?.(10)
    // 提取iv
    const iv = new Uint8Array(fileBuffer.slice(offset, offset + FileHeaderAes.IV_SIZE));
    offset += FileHeaderAes.IV_SIZE;
    progress?.(20)
    // 读取count
    const count = FileHeaderAes.byteArrayToInt(new Uint8Array(fileBuffer.slice(offset, offset + 4)));
    offset += 4;
    progress?.(30)
    // 读取size
    const size = FileHeaderAes.byteArrayToInt(new Uint8Array(fileBuffer.slice(offset, offset + 4)));
    offset += 4;
    progress?.(40)
    const initialLength = count * size;
    let totalLength = initialLength;
    const encryptedContents: Uint8Array[] = [];

    if (initialLength) {
      const encryptedContent = new Uint8Array(fileBuffer.slice(offset, offset + initialLength));
      offset += initialLength;
      encryptedContents.push(encryptedContent);
    }
    progress?.(50)
    while (offset < byteLength) {
      const segmentSize = FileHeaderAes.byteArrayToInt(new Uint8Array(fileBuffer.slice(offset, offset + 4)));
      totalLength += segmentSize;
      offset += 4;

      const encryptedContent = new Uint8Array(fileBuffer.slice(offset, offset + segmentSize));
      offset += segmentSize;
      encryptedContents.push(encryptedContent);
      console.log('encryptedContents.length', encryptedContents.length);
      if (progress) {
        const percent = offset / byteLength
        progress?.(((percent * 25) + 50))
      }
    }

    const mergedArray = new Uint8Array(totalLength);
    let mergeOffset = 0;
    let i = 0
    for (const content of encryptedContents) {
      mergedArray.set(content, mergeOffset);
      mergeOffset += content.byteLength;
      progress?.((((i++ / (encryptedContents.length - 1)) * 25 + 76)))
    }

    return {encryptedContent: mergedArray, iv, salt};
  }

  // 解密文件
  async decrypt(file: File | Promise<ArrayBuffer>, password: string, progress?: (percent: number) => void): Promise<Blob> {
    if (!password) {
      throw new Error('Please enter a password.');
    }

    const fileBuffer = await readFileAsArrayBuffer(file);
    if (progress) {
      progress(20);
    }

    let data: { encryptedContent: Uint8Array; iv: Uint8Array; salt: Uint8Array };
    const prefix = new Uint8Array(fileBuffer.slice(0, FileHeaderAes.BYTES_PREFIX.length));
    if (prefix.every((v, i) => v === FileHeaderAes.BYTES_PREFIX[i])) {
      data = this.getSegmentEncryptData(fileBuffer, (value: number) => progress?.(value / 100 * 80 + 10));
    } else {
      data = {
        salt: new Uint8Array(fileBuffer.slice(0, 16)),
        iv: new Uint8Array(fileBuffer.slice(16, 32)),
        encryptedContent: new Uint8Array(fileBuffer.slice(32)),
      };
    }

    try {
      const cryptoKey = await this.deriveKey(password, data.salt);
      const decryptedContent = await crypto.subtle.decrypt(
          {
            name: 'AES-CBC',
            iv: data.iv,
          },
          cryptoKey,
          data.encryptedContent,
      );

      if (progress) {
        progress(90);
      }

      return new Blob([decryptedContent], {type: 'application/octet-stream'});
    } finally {
      if (progress) {
        progress(100);
      }
    }
  }
}
